feat(workflows): expose {{ context.run_id }} template variable#2664
Conversation
9940396 to
68634d8
Compare
|
@mnriem — quick status on this one: no outstanding review comments (the 2026-05-27 Copilot pass reported 0 new findings), and I've now rebased the branch onto current The failing checks visible on the PR are not a code issue — they're a transient GitHub Actions infrastructure failure from 2026-05-26 12:19 UTC (the runs were queued 2026-05-21 in That SHA is the pin for Branch is AI disclosure: drafted with Claude Opus, human-reviewed. |
Closes github#2590. Surfaces the engine-assigned run id (the same 8-character hex string Spec Kit prints as `Run ID:` at the end of `workflow run`) as a workflow template variable so YAML authors can reference it from shell `run:`, command `input.args:`, switch `expression:`, and any other field that already evaluates `{{ ... }}` templates. ### Why The run id is the natural join key between a Spec Kit workflow run and downstream artifacts, telemetry, or per-run scratch state. Today the operator sees it in stdout but workflows themselves cannot reference it — there was no way to stamp a log line, name a scratch directory, or tag an artifact with the same id Spec Kit assigned. The three motivating use cases from the issue: 1. Telemetry / observability — stamp logs and events with the run id so external systems can join workflow runs to downstream artifacts. 2. Per-run scratch / isolation — interactive operator commands that need their own state directory under `/tmp/run-<id>/`. 3. Run-id in artifact metadata — stable join key from artifact back to the producing run. ### Implementation `StepContext.run_id` is already populated by `WorkflowEngine` in both `execute()` and `resume()`. The only gap was the template namespace builder. `_build_namespace` (in `workflows/expressions.py`) now adds a `context` key alongside the existing `inputs`, `steps`, `item`, and `fan_in` namespaces: ```python ns["context"] = {"run_id": run_id} ``` The value is always present (even outside a run) and falls back to an empty string when no run is active. Workflows referencing `{{ context.run_id }}` therefore never error — a hard requirement from the issue's acceptance criteria for dry-run, validation, and ad-hoc evaluator usage. ### Default behaviour preserved Workflows that do not reference `{{ context.run_id }}` are byte-equivalent to before this change. The `context` namespace is added unconditionally to keep template resolution branch-free, but its presence has no observable effect when nothing references it. ### Tests `TestExpressions` (unit-level) gains three tests: - `test_context_run_id_resolves` — direct lookup against a `StepContext(run_id=...)`. - `test_context_run_id_defaults_to_empty_when_unset` — graceful default outside a run context. - `test_context_run_id_string_interpolation` — mixed template (e.g. `"RUN_ID={{ context.run_id }}"`). `TestContextRunId` (end-to-end) covers the three step types the acceptance criteria called out: - `test_shell_run_resolves_run_id` — `run:` field substitution, verified via captured stdout. - `test_command_input_args_resolves_run_id` — `input.args:` resolution, captured in step output even when CLI dispatch is unavailable (the artifact-metadata use case). - `test_switch_expression_matches_on_run_id` — switch matches against the resolved value, proving the run id is a first-class value in the expression engine, not just an interpolation token. - `test_workflow_without_context_reference_unchanged` — locks the byte-equivalent default required by the issue. ### Docs `workflows/README.md` gains a "Runtime Context" subsection under "Expressions" documenting the new namespace and the three canonical use patterns (telemetry, per-run scratch, artifact metadata).
68634d8 to
20b71ba
Compare
mnriem
left a comment
There was a problem hiding this comment.
Please address Windows tests errors
`test_shell_run_resolves_run_id` and
`test_switch_expression_matches_on_run_id` used
`run: 'echo "RUN_ID={{ context.run_id }}"'` with inner double-quotes
around the echo argument. Bash/sh strips those quotes before invoking
echo, but cmd.exe (used on Windows when `shell=True`) treats them
as literal characters and emits `"RUN_ID=abc12345"` — failing the
exact-match assertion. Linux passed; all three Windows-latest matrix
entries failed with `assert '"RUN_ID=abc12345"' == 'RUN_ID=abc12345'`.
Resolve by dropping the inner double-quotes (the value has no spaces
or shell metacharacters) and wrapping the YAML scalar in plain
double-quotes the same way other shell-step tests in this file do
(e.g. `run: "echo b-saw-..."`). Behaviour-equivalent on POSIX,
portable to cmd.exe.
|
@mnriem — Windows pytest failure fixed in Root cause. Two tests in run: 'echo "RUN_ID={{ context.run_id }}"'Bash/sh strips those inner double-quotes before invoking (The Windows-3.13 job log shows a Fix. Drop the inner double-quotes (the value has no spaces or shell metacharacters that need them) and wrap the YAML scalar in plain double-quotes the same way other shell-step tests in Diff is two lines: - run: 'echo "RUN_ID={{ context.run_id }}"'
+ run: "echo RUN_ID={{ context.run_id }}"
...
- run: 'echo "nested-run-id={{ context.run_id }}"'
+ run: "echo nested-run-id={{ context.run_id }}"Full AI disclosure: drafted with Claude Opus, human-reviewed. |
|
Thank you! |
Adds an optional `continue_on_error: bool` field on every step.
When set to `true` and the step fails, the engine records the
result (`exit_code`, `stderr` on `steps.<id>.output` plus `status`
as a sibling key on `steps.<id>`) and continues to the next sibling
step instead of halting the run. Downstream `if`, `switch`, or
`gate` steps can then branch on
`{{ steps.<id>.output.exit_code }}` to route the recovery path.
Engine details
--------------
`WorkflowEngine._execute_steps` now consults the step config when a
step returns `StepStatus.FAILED`:
- Gate aborts (`output.aborted`) always halt the run — operator
decisions take precedence over the flag.
- Otherwise, if `continue_on_error` is the literal `True`, log a
`step_continue_on_error` event and proceed to the next sibling.
The runtime check uses identity comparison (`is True`) rather
than truthiness, so truthy non-bool values like the string
`"true"` cannot silently change run semantics even if a caller
bypasses `validate_workflow()`.
- Otherwise, behave as before: log `step_failed`, set
`RunStatus.FAILED`, and return.
Validation
----------
`_validate_steps` rejects non-bool values for `continue_on_error`.
Coerced strings like `"true"` are not accepted so authoring
mistakes surface at validation time rather than silently changing
run semantics.
Tests
-----
`TestContinueOnError` in `tests/test_workflows.py` (8 tests):
- `test_undeclared_failure_halts_run` — default halt behaviour.
- `test_declared_and_fired_continues_run` — flag + fail → continue.
- `test_declared_but_step_succeeded_is_noop` — flag + success → no-op.
- `test_if_branch_routes_around_failure` — end-to-end recovery.
- `test_gate_abort_still_halts_with_continue_on_error` — abort
always halts.
- `test_validation_rejects_non_bool_continue_on_error` — `"true"`
rejected at validation.
- `test_validation_accepts_bool_continue_on_error` — `true`/`false`
pass cleanly.
- `test_engine_ignores_truthy_non_bool_continue_on_error` —
defense-in-depth: engine ignores string `"true"` even when
validation is bypassed.
Rebased onto current upstream/main (post github#2664 merge); the new
`TestContinueOnError` class sits immediately after upstream's
`TestContextRunId` so the two feature suites coexist cleanly.
Closes github#2591.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Description
Closes #2590.
Surfaces the engine-assigned run id (the same 8-character hex
string Spec Kit prints as
Run ID:at the end ofworkflow run) as a workflow template variable so YAML authorscan reference it from shell
run:, commandinput.args:,switch
expression:, and any other field that already evaluates{{ ... }}templates.This is shape A from the issue (
{{ context.run_id }}) —the most discoverable option and consistent with the existing
inputs.*/steps.X.output.*naming.Why
The run id is the natural join key between a Spec Kit workflow
run and downstream artifacts, telemetry, or per-run scratch
state. Today the operator sees it in stdout but workflows
themselves cannot reference it — there was no way to stamp a
log line, name a scratch directory, or tag an artifact with the
same id Spec Kit assigned.
The three use cases from the issue:
the run id so external systems can join workflow runs to
downstream artifacts.
commands that need their own state directory under
/tmp/run-<id>/.artifact back to the producing run.
Canonical usage
Implementation
StepContext.run_idis already populated byWorkflowEnginein both
execute()andresume(). The only gap was thetemplate namespace builder.
_build_namespace(inworkflows/expressions.py) now adds acontextkey alongside the existinginputs,steps,item,and
fan_innamespaces:The value is always present (even outside a run) and falls back
to an empty string when no run is active. Workflows referencing
{{ context.run_id }}therefore never error — a hardrequirement from the issue's acceptance criteria for dry-run,
validation, and ad-hoc evaluator usage.
Default behaviour preserved
Workflows that do not reference
{{ context.run_id }}arebyte-equivalent to before this change. The
contextnamespaceis added unconditionally to keep template resolution
branch-free, but its presence has no observable effect when
nothing references it.
Testing
uv run specify --helpuv sync && uv run pytest→ 2967 passed, 35 skipped (was 2960 before; +7 new
tests added in this PR).
run: 'echo "RUN_ID={{ context.run_id }}"'and confirmedthe captured stdout matches the
Run ID:line Spec Kitprints at the end of
workflow run. Re-ran without thetemplate reference and the workflow behaved identically
to pre-PR.
New test coverage
TestExpressions(unit-level):test_context_run_id_resolvesStepContext(run_id=...).test_context_run_id_defaults_to_empty_when_unsettest_context_run_id_string_interpolation"RUN_ID={{ context.run_id }}".TestContextRunId(end-to-end), covering the three step types the issue's acceptance criteria called out:test_shell_run_resolves_run_idrun:field substitution, verified via captured stdout.test_command_input_args_resolves_run_idinput.args:resolution, captured in step output even when CLI dispatch is unavailable (the artifact-metadata use case).test_switch_expression_matches_on_run_idtest_workflow_without_context_reference_unchangedAI Disclosure
Used Claude Opus to draft the namespace change, the test suite,
the docs section, and this PR body. The shape
(
{{ context.run_id }}with empty-string fallback) wasproposed in the issue body; this PR implements that proposal.
Code, tests, and design decisions were human-reviewed before
submission.